import fm
import os
import wx
import debug
import wx.stc as stc
import modes

from debug  import here,pr
from base   import *

settings=fm.settings
persistent_state=fm.persistent_state

import brace_match, line_mod_status



settings['status_bar.line_info_fmt']='Line %(line)s\%(char)s'
settings['status_bar.line_info_fmt :help']='Full example: "[%(top_line)s..%(bottom_line)s] Line %(line)s\%(char)s"'

class LinesModificationStatus:
			
	def __init__(me):
		
		me.line_state = []
		
	def update_opened(me,e):
		me.line_state=[fm.UNCHANGED]*e.src.text.GetLineCount() #?? hide stc
		
	def update_saved(me,e):
		for i in range(len(me.line_state)):
			if me.line_state[i]==fm.UNSAVED:
				me.line_state[i]=fm.SAVED
				
	def update_lines_added(me,e):
		if e.N_lines_added==1 and e.first_added_line==len(me.line_state)-1: # this is the last line being appended, already seen it
			return
			
		me.line_state[e.first_added_line:e.first_added_line]=[fm.UNSAVED]*e.N_lines_added

	def update_lines_deleted(me,e):
		me.line_state[e.first_deleted_line:e.first_deleted_line+e.N_lines_deleted]=[]
		
	def update_line_changed(me,e):
		"returns whether the line's modification status has changed"
		
		if e.changed_line == len(me.line_state): # line appended, not yet piped through lines_added()
			me.line_state.append(fm.UNSAVED)
			return True
			
		if me.line_state[e.changed_line]<>fm.UNSAVED: # ?? bug IndexError
			me.line_state[e.changed_line]=fm.UNSAVED
			return True
			
		return False

#settings['eol_mode']='platform' #?? this doesn't work, set it to 'unix', and it inserts empty line, replace with 'doc.eol_mode_for_new_files'
#settings['eol_mode :type']=('platform','unix','mac','win')
settings['wrap']=False
settings['wrap_mode']='word'
settings['wrap_mode :type']=('char','word')
settings['show_line_numbers']=False
settings['whitespace_visibility']='invisible'
settings['whitespace_visibility :type']=('invisible','visible_always','visible_after_indent')
settings['indent']=8
settings['indent.tab_width']=8
settings['indent.mode']='use_mixture_of_tabs_and_spaces'
settings['indent.mode :type']=('use_only_spaces', 'use_mixture_of_tabs_and_spaces')
		
class Document:
	
	MARGIN__LINE_NRS=0
	MARGIN__LINE_MOD_STATUS=1
	
	MARKER__CHANGED_AND_SAVED=0
	MARKER__UNSAVED=1
	
	EVTMSK_UPDATEUI,     \
	EVTMSK_TEXTMODIFIED, \
	EVTMSK_PAINTED,      \
	EVTMSK_CHARADDED,    \
	EVTMSK_SAVED         = mask_set(5)
	
	def __init__(me,parent,file=None):
		me.events = fm.EventSet(
			'Opened','Saved','Unsaved',
			'LinesAdded','LinesDeleted','LineChanged',
			'ActiveStateToggleRequest'
		)
	
		# ?? what happens if file does not exist?
		
		me.EVT = me.events #?? get rid of me.events
		
		textbar=fm.Textbar(me,parent)
		
		me.Edit=wx.stc.StyledTextCtrl(parent)
		
		me.text=me.Edit # ?? get rid
		me.edit=me.Edit # ?? get rid
		
		me.modified = False
		me.active   = True
		me.filepath = file # ?? get rid
		me.File     = file
		me.textbar  = textbar
		me.parent   = parent
		me.mode     = fm.DefaultMode
				
		me.mode.style_stc(me.edit)
				
		me.lines_modification_status = LinesModificationStatus()
		
		if me.File:
			me.File = os.path.abspath(me.File)
			me.edit.LoadFile(me.File)
			
			e=struct(src=me)
		
			me.lines_modification_status.update_opened(e)
											
			me.EVT.Opened.broadcast( e )
			
			
		me.Edit.SetFocus()
		
		me.wire_events()
		
		me.reconfigure(e=None,__init__=True)
		
		me.brace_match_controller = brace_match.BraceMatchController(me)
		
		me.Model_LineModStatus    = line_mod_status.LineModStatusModel(me)
		me.margin_line_mod_status = line_mod_status.DocMarginView(me,me.Model_LineModStatus)
						
		me.update_textbar_w_line_dimensions()
		me.update_textbar_w_view_position()

	def wire_events(me):
		
		fm.EventBus.AddBroadcaster(me)
		
		settings.on_change_call( me.reconfigure )
		
		# ---------- TEXTBAR ----------
							
		me.textbar.Bind(wx.EVT_LEFT_DOWN, me.OnTextBarMouseLeftDown) #?? perhaps do this kosher w event propagation
		me.textbar.events.Scroll += me.on_textbar_scroll
		
		# ---------- FIND ----------
		
		me.bind__(wx.EVT_FIND,             me.OnFind)
		me.bind__(wx.EVT_FIND_NEXT,        me.OnFind)
		me.bind__(wx.EVT_FIND_REPLACE,     me.OnFind)
		me.bind__(wx.EVT_FIND_REPLACE_ALL, me.OnFind)
		me.bind__(wx.EVT_FIND_CLOSE,       me.OnFindClose)
		
		# ----------- SCINTILLA ----------
		
		me.parent.Bind(wx.stc.EVT_STC_PAINTED,    me.OnTextPainted,   me.edit)
		me.parent.Bind(wx.stc.EVT_STC_MODIFIED,   me.OnTextModified,  me.edit)
		me.parent.Bind(wx.stc.EVT_STC_CHARADDED,  me.OnTextCharAdded, me.edit)
		me.parent.Bind(wx.stc.EVT_STC_UPDATEUI,   lambda ev: fm.EventBus.Broadcast(me,me.EVTMSK_UPDATEUI), me.edit)

		
	def reconfigure(me,e,__init__=False):
		
		tx=me.text
		
		if __init__:			
			tx.SetUseVerticalScrollBar(False)
			
			# ?? configurable indentation behavior
			tx.SetIndent(0) # ?? if set to one, becomes slooooow for really long lines
			tx.SetTabIndents(True)
			tx.SetUseTabs(True)
			tx.SetBackSpaceUnIndents(True)

		
		if 1:
			if settings['wrap']:
				tx.SetWrapMode(stc.STC_WRAP_WORD)
				tx.SetWrapStartIndent(32) # ?? how to make this relative to line indent
				tx.SetWrapVisualFlags(stc.STC_WRAPVISUALFLAG_START)
				tx.SetWrapVisualFlagsLocation(stc.STC_WRAPVISUALFLAGLOC_START_BY_TEXT)
				tx.SetLayoutCache(stc.STC_CACHE_NONE)
			else:
				tx.SetWrapMode(stc.STC_WRAP_NONE)
			
		if 1:
			flag = stc.STC_WS_INVISIBLE
			if settings['whitespace_visibility']=='visible_after_indent':
				flag=stc.STC_WS_VISIBLEAFTERINDENT
			elif settings['whitespace_visibility']=='visible_always':
				flag=stc.STC_WS_VISIBLEALWAYS
			tx.SetViewWhiteSpace(flag)
			
		if 1:
			tx.SetIndent(settings['indent'])
			
			if settings['indent.mode']=='use_mixture_of_tabs_and_spaces':
				tx.SetUseTabs(True)
			else:
				tx.SetUseTabs(False)
				
			tx.SetTabWidth(settings['indent.tab_width'])

				
		me.show_margin_line_nrs(settings['show_line_numbers'])
			
	def bind__(me,e,handler):
		"since we are not window, we resort to this"
		me.text.Bind(e,handler)
		me.textbar.Bind(e,handler)
		
	def show_find_dialog(me):
		sel=me.text.GetSelectedTextRaw() # ?? unicode
		data = wx.FindReplaceData(wx.FR_DOWN)
		if sel: data.SetFindString(sel) # ?? also try to get from clipboard
		elif persistent_state['last_find_string']: data.SetFindString(persistent_state['last_find_string'])
		d = wx.FindReplaceDialog(me.text, data, "Find") # ?? opens with find string selected
		d.data = data  # save a reference to it...
		d.Show(True)

	def get_lines_modification_status(me):
		return me.lines_modification_status.line_state
			
	def show_margin_line_nrs(me,show=True):
		if show:
			me.margin_line_nrs_shown=True
			me.text.SetMarginType(me.MARGIN__LINE_NRS,stc.STC_MARGIN_NUMBER)
			me.margin_line_nrs_max_digits=None # will be picked up in handler, and the width will be set accordingly
			me.update_margin_line_nrs_width()
		else:
			me.margin_line_nrs_max_digits=None
			me.margin_line_nrs_shown=False
			me.text.SetMarginWidth(me.MARGIN__LINE_NRS,0)
			
	def update_margin_line_nrs_width(me):
		if not me.margin_line_nrs_shown:
			return
			
		n=me.text.GetLineCount()
		max_digits=int(math.ceil(math.log(n+1,10)))
		
		if max_digits==me.margin_line_nrs_max_digits:
			return
			
		me.margin_line_nrs_max_digits=max_digits
		
		#print max_digits
		
		widest_string="9"*max(3,max_digits+1)
		width=me.text.TextWidth(stc.STC_STYLE_LINENUMBER,widest_string)
		me.text.SetMarginWidth(me.MARGIN__LINE_NRS,width)
		
	def update_status(me,force=False):
		t=me.text
		pos=t.GetCurrentPos()
		try:
			if not force and me.status_last_pos_guard==pos:
				return
		except AttributeError:
			pass
		me.status_last_pos_guard=pos
		top_line = t.GetFirstVisibleLine()+1
		bottom_line = top_line+t.LinesOnScreen()+1
		char=t.GetColumn(pos)
		line=t.LineFromPosition(pos)+1
		status_string=settings['status_bar.line_info_fmt']%{'line':line,'char':char,'top_line':top_line,'bottom_line':bottom_line}
		# due to a buglet, at least on osx, delay status bar call until after this event
		# is processed, else you get spurios "CGContextRestoreGState: invalid context" msgs
		#me.parent.SetStatusTextRightField(status_string)
		wx.CallAfter(fm.main_frame.SetStatusTextRightField,status_string)
		#print 'setting'
				
	# - wx event handlers
	def OnFind(me,e):
		et      = e.GetEventType()
		flags   = e.GetFlags()
		
		from_beginning   = True if et == wx.wxEVT_COMMAND_FIND else False
		find_string      = e.GetFindString()
		replace_string   = e.GetReplaceString()
		match_case       = flags & wx.FR_MATCHCASE
		whole_word       = flags & wx.FR_WHOLEWORD
		prev             = not (flags & wx.FR_DOWN)
		
		persistent_state['last_find_string'] = find_string
		
		r=me.find(find_string,from_beginning=from_beginning,prev=prev,match_case=match_case,whole_word=whole_word)
		
		if r<>-1:
			e.GetDialog().Destroy()
			fm.short_duration_status_text('Continue searching for "%s" with \'Edit->Find Next\' and \'Edit->Find Prev\''%find_string,milliseconds=10000)
		
	def OnFindClose(me,e): e.GetDialog().Destroy()

	def OnTextCharAdded(me,event):
		# ?? do this right with autotext.py
		
		ch=event.GetKey()
		if ch==ord("\n"):#2fix - what about "\r\n"?
			line=me.text.GetCurrentLine()
			if line>0:
				indent=me.text.GetLineIndentation(line-1)
				prevline = me.text.GetLine(line-1)
				indent_string=''
				if indent>0:
					i = 0
					indent_levels=0
					while prevline[i] == '\t':
						indent_levels+=1
						i=i+1
					#print '%r'%prevline
					# ?? only decrease if prevline and prevprevline are empty
					# ?? XXX yikes, bused errored out of here, wiped out my file! (IndexError followed by Bus Error)
					#if len(prevline)>2 and prevline[-3]=='\t':# and line>=2 and me.text.GetLine(line-2)[-3]=='\t': #?? sooo hackish
					#	indent_levels-=1
					indent_string+='\t'*indent_levels
					
				if len(prevline)>1 and prevline[-2] == ":" or len(prevline)>2 and prevline[-3]==":":#2fix - must be a better way to do this too
					indent_string+='\t'
				
				me.text.AddText(indent_string) # ?? - make this kosher, indent might not be 3		

	def OnTextPainted(me,e):
		me.update_textbar_w_view_position()
		me.update_status()

		
	def OnTextModified(me,e):
		if (not e.ModificationType & stc.STC_MOD_INSERTTEXT) and (not e.ModificationType & stc.STC_MOD_DELETETEXT):
			return # don't care about style changed
		
		if not me.modified:
			me.modified=True
			me.events.Unsaved(src=me)
		
		line=me.text.LineFromPosition(e.GetPosition())
		N_lines_changed=e.GetLinesAdded()
		
		if N_lines_changed:
			me.update_margin_line_nrs_width()
			
		if N_lines_changed<0: # LINES DELETED
			
			e=struct(src=me,first_deleted_line=line,N_lines_deleted=N_lines_changed)

			me.lines_modification_status.update_lines_deleted( e )
			
			#me.MODL_line_mod_status.broadcast_del_lines(line+1,line+1+(-N_lines_changed)) # ?? for some reason, stc obob
			
			me.EVT.LinesDeleted.broadcast( e )

		elif N_lines_changed>0: # LINES ADDED
			
			e=struct(src=me,first_added_line=line,N_lines_added=N_lines_changed)

			me.lines_modification_status.update_lines_added( e )
			
			# if len is zero, the file is just opening, and a special function will be called
			#me.MODL_line_mod_status.broadcast_add_lines(line,N_lines_changed)
			#me.MODL_line_mod_status.Broadcast_AddLines(line,N_lines_changed)
			
			me.EVT.LinesAdded.broadcast( e )
			
		else: # LINE CHANGED
			e=struct(src=me,changed_line=line)
						
			e.line_modification_status_changed = me.lines_modification_status.update_line_changed( e )
						
			#me.MODL_line_mod_status.broadcast_chg_line(line)
			
			me.EVT.LineChanged.broadcast( e )
			
		i = line
		n = N_lines_changed
		
		fm.EventBus.Broadcast(me,me.EVTMSK_TEXTMODIFIED,(i,n))
			
		if me.textbar.N_lines==me.text.GetLineCount():
			return
		
		#me.MODL_line_dimensions.Broadcast_Refresh()
		#me.MODL_view_position.Broadcast_Refresh()
		me.update_textbar_w_line_dimensions()
		me.update_textbar_w_view_position()
		
	def OnTextBarMouseLeftDown(me,e):
		me.events.ActiveStateToggleRequest(src=me)
		e.Skip()
		
	# - </wx event handlers>
	
	# - textbar event handlers
	
	def on_textbar_scroll(me,e):
		top_line=max(0,e.line-me.text.LinesOnScreen()/2)
		me.text.ScrollToLine(top_line)
		me.update_textbar_w_view_position()
	
	# - </textbar event handlers>

						
	def save_as(me):
		me.save(save_as=True)
		
	def save(me,save_as=False,message="Save file as ..."):
		if save_as or not me.filepath:
			d=wx.FileDialog(me.parent,message=message,style=wx.SAVE|wx.CHANGE_DIR)
			if d.ShowModal()<>OK:
				return CANCEL
			me.filepath=os.path.abspath(d.GetPath())
			me.modified=True # as in "my file changed"
			
			me.set_proper_mode__()

		if not me.modified:
			return OK
			
		#me.text.SaveFile(me.file)
		content = me.text.GetText()
		fm.safer_save(me.filepath,content)
		
		me.modified=False
		
		e=struct(src=me)
		
		me.lines_modification_status.update_saved( e )
		
		#me.MODL_line_mod_status.broadcast_file_saved()
		
		"""
		for i,s in enumerate(me.get_lines_modification_status()):
			if s==fm.UNCHANGED:continue
			reset_all_to_untouched
			#if s==fm.SAVED:
			#	me.margin_line_modification_status.mark_line_saved( i )
		"""

		
		me.EVT.Saved.broadcast( e )
		
		me.set_proper_mode__()
		
		fm.EventBus.Broadcast(me,me.EVTMSK_SAVED)
		
		return OK
		
	def get_filepath(me):
		return me.filepath
		
	def set_proper_mode__(me):
		mode=modes.detect_file_mode_from_file_name(me.filepath)
		if mode==me.mode: return
		mode.style_stc(me.text)
		me.mode=mode
		#print me.file,mode.name
	"""	
	def xopen(me,file):
		assert not me.modified, "expect caller to save, close 1st, whatever is necessary"
		assert not me.file, "can not open a file again in a document with file already open"
		
		me.events.LinesAdded.block()
		me.events.LineChanged.block()
		me.file_being_opened = True
		try:
			me.text.LoadFile(file) # triggers, the now blocked, modify event
		finally:
			me.events.LinesAdded.unblock()
			me.events.LineChanged.unblock()
			me.file_being_opened = False
		
		me.modified=False
		
		me.file=os.path.abspath(file)
		
		me.set_proper_mode__()
		
		e=struct(src=me)
		
		me.lines_modification_status.update_opened(e)
		
		print 'open("%s")'%file
		
		me.MODL_line_mod_status.broadcast_file_open(me.edit.GetLineCount())
							
		me.EVT.Opened.broadcast( e )
		
		me.update_textbar_w_line_dimensions()
		me.update_textbar_w_view_position()
	"""
		
	def update_textbar_w_line_dimensions(me):
		N_lines=me.text.GetLineCount()
		#indents=[me.text.GetLineIndentation(i) for i in range(N_lines)]
		#lengths=[me.text.GetLineEndPosition(i) - me.text.PositionFromLine(i) for i in range(N_lines)]
		#^we don't use LineLength() because we do not want to count the end of the line characters
		#lengths=[me.text.GetLineEndPosition(i)-me.text.GetLineIndentPosition(i) for i in range(N_lines)]
		indents=[]
		lengths=[]
		for line_nr in xrange(N_lines):
			
			indent=me.text.GetLineIndentation(line_nr)
			length=me.text.GetColumn(me.text.GetLineEndPosition(line_nr))
			"""if line_nr==0:
				length=me.text.GetLineEndPosition(line_nr)
			else:
				length=me.text.GetLineEndPosition(line_nr)-me.text.GetLineEndPosition(line_nr-1)"""
			
			indents.append(indent)
			lengths.append(length)
			
		if 0:
			for i in xrange(N_lines):
				print indents[i],lengths[i],'%r'%me.text.GetLine(i)
			
		#here('linedim')
		me.textbar.set_line_dimensions(indents,lengths)
		me.textbar.Refresh()
	
	"""
	def visible_lines(me):
		top=me.text.GetFirstVisibleLine()
		display_lines=me.text.LinesOnScreen()+1
		real_lines=0
		n=0
		while n < display_lines:
			n += me.text.WrapCount(top+real_lines)
			real_lines+=1
		return top,real_lines-1
	"""
		
	def update_textbar_w_view_position(me):
		N_visible_lines   = me.text.LinesOnScreen()+1 # +1 is for partially visible line
		top_visible_line  = me.text.GetFirstVisibleLine()

		#top_visible_line, N_visible_lines = me.visible_lines()
		
		if N_visible_lines <> me.textbar.N_visible_lines or top_visible_line <> me.textbar.top_visible_line:
			#print 'top visible',top_visible_line,N_visible_lines
			me.textbar.N_visible_lines=N_visible_lines
			me.textbar.top_visible_line= top_visible_line
			me.textbar.refresh_thumb()
	
	def activate(me,activate=True):
		if me.active==activate:
			return
		me.active=activate
		me.textbar.activate(activate)
		
		if activate:
			#print "** activate: calling update_status()"
			me.update_status(force=True)

	# - text ops
			
	def cut(me):     me.text.Cut()
	def undo(me):    me.text.Undo()
	def copy(me):    me.text.Copy()
	def redo(me):    me.text.Redo()
	def paste(me):   me.text.Paste()	

	def find(me,string=None,from_beginning=False,prev=False,match_case=False,whole_word=False):
		if from_beginning:
			me.text.GotoPos(0)
			fm.short_duration_status_text('Searching from the beginning of file')
			
		if not string: string=me.text.GetSelectedTextRaw() # ?? unicode # ?? document this behavior
		if not string:
			string=persistent_state['last_find_string']
			fm.short_duration_status_text('Searching for "%s"'%string)
		if not string: return
			
		me.text.SearchAnchor()
		
		flags=0
		if whole_word: flags|=stc.STC_FIND_WHOLEWORD
		if match_case: flags|=stc.STC_FIND_MATCHCASE
		
		r=-1
		
		if prev:
			r=me.text.SearchPrev(flags,string)
		else:
			sel=me.text.GetSelectedTextRaw() # ?? unicode
			if sel==string:
				pos=me.text.GetCurrentPos()
				me.text.GotoPos(pos+len(string))
				me.text.SearchAnchor()
			r=me.text.SearchNext(flags,string)
		
		if r==-1:
			fm.short_duration_status_text('Not found')
			
		me.text.EnsureCaretVisible()
		
		return r
		
	def set_eol_mode(me,mode):
		if mode=='win': mode=stc.STC_EOL_CRLF
		elif mode=='mac': mode=stc.STC_EOL_CR
		elif mode=='unix': mode=stc.STC_EOL_LF
		else:
			raise ValueError("Don't know eol mode '%s', allowable values are 'win','unix','mac'")
		me.text.SetEOLMode(mode)
		me.text.ConvertEOLs(mode)
		#print 'set',mode,'get',me.text.GetEOLMode()
		
	def get_eol_mode(me):
		mode=me.text.GetEOLMode()
		#print mode
		if mode==stc.STC_EOL_CRLF: return 'win'
		if mode==stc.STC_EOL_CR: return 'mac'
		if mode==stc.STC_EOL_LF: return 'unix'
		assert 0		
	
	# </text ops>
	
	def Destroy(me):
		me.textbar.Destroy()
		me.text.Destroy()


		
	